热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

屏障|带子_聊一聊如何用C#轻松完成一个SAGA分布式事务

篇首语:本文由编程笔记#小编为大家整理,主要介绍了聊一聊如何用C#轻松完成一个SAGA分布式事务相关的知识,希望对你有一定的参考价值。背景

篇首语:本文由编程笔记#小编为大家整理,主要介绍了聊一聊如何用C#轻松完成一个SAGA分布式事务相关的知识,希望对你有一定的参考价值。



背景


银行跨行转账业务是一个典型分布式事务场景,假设 A 需要跨行转账给  B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的 ACID ,只能够通过分布式事务来解决。


市面上使用比较多的分布式事务框架,支持 SAGA 的,大部分都是 JAVA 为主的,没有提供 C# 的对接方式,或者是对接难度大,一定程度上让人望而却步。


这里推荐一下叶东富大佬的分布式事务框架 dtm,一款跨语言的开源分布式事务管理器,优雅的解决了幂等、空补偿、悬挂等分布式事务难题。提供了简单易用、高性能、易水平扩展的分布式事务解决方案。


老黄在搜索相关分布式事务资料的时候,他写的文章都是相对比较好理解的,也就是这样关注到了 dtm 这个项目。


下面就基于这个框架来实践一下银行转账的例子。


前置工作


dotnet add package Dtmcli --version 0.3.0

成功的 SAGA


先来看一下一个成功完成的 SAGA 时序图。



上图的微服务1,对应我们示例的 OutApi,也就是转钱出去的那个服务。


微服务2,对应我们示例的 InApi,也就是转钱进来的那个服务。


下面是两个服务的正向操作和补偿操作的处理。


OutApi


app.MapPost("/api/TransOut", (string branch_id, string gid, string op, TransRequest req) => 
    // 进行 数据库操作
    Console.WriteLine($"用户【req.UserId】转出【req.Amount】正向操作,gid=gid, branch_id=branch_id, op=op");
    return Results.Ok(TransResponse.BuildSucceedResponse());
);
app.MapPost("/api/TransOutCompensate", (string branch_id, string gid, string op, TransRequest req) =>
    // 进行 数据库操作
    Console.WriteLine($"用户【req.UserId】转出【req.Amount】补偿操作,gid=gid, branch_id=branch_id, op=op");
    return Results.Ok(TransResponse.BuildSucceedResponse());
);

InApi


app.MapPost("/api/TransIn", (string branch_id, string gid, string op, TransRequest req) =>
    Console.WriteLine($"用户【req.UserId】转入【req.Amount】正向操作,gid=gid, branch_id=branch_id, op=op");
    return Results.Ok(TransResponse.BuildSucceedResponse());
);
app.MapPost("/api/TransInCompensate", (string branch_id, string gid, string op, TransRequest req) =>
    Console.WriteLine($"用户【req.UserId】转入【req.Amount】补偿操作,gid=gid, branch_id=branch_id, op=op");
    return Results.Ok(TransResponse.BuildSucceedResponse());
);


注:示例为了简单,没有进行实际的数据库操作。



到此各个子事务的处理已经 OK 了,然后是开启 SAGA 事务,进行分支调用


var userOutReq = new TransRequest()  UserId = "1", Amount = -30 ;
var userInReq = new TransRequest()  UserId = "2", Amount = 30 ;
var ct = new CancellationToken();
var gid = await dtmClient.GenGid(ct);
var saga = new Saga(dtmClient, gid)
    .Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq)
    .Add(inApi + "/TransIn", inApi + "/TransInCompensate", userInReq)
    ;
var flag = await saga.Submit(ct);
Console.WriteLine($"case1, gid saga 提交结果 = flag");

到这里,一个完整的 SAGA 分布式事务就编写完成了。


搭建好 dtm 的环境后,运行上面的例子,会看到下面的输出。



当然,上面的情况太理想了,转出转入都是一次性就成功了。


但是实际上我们会遇到许许多多的问题,最常见的应该就是网络故障了。


下面来看一个异常的 SAGA 示例


异常的 SAGA


做一个假设,用户1的转出是正常的,但是用户2在转入的时候出现了问题。


由于事务已经提交给 dtm 了,按照 SAGA 事务的协议,dtm 会重试未完成的操作。


这个时候用户2 这边会出现什么样的情况呢?


  1. 转入其实成功了,但是 dtm 收到错误 (网络故障等)

  2. 转入没有成功,直接告诉 dtm 失败了 (应用异常等)


无论是那一种,dtm 都会进行重试操作。这个时候会发生什么呢?我们继续往下看。


先看一下事务失败交互的时序图



再通过调整上面成功的例子,来比较直观的看看出现的情况。


在 InApi 加多一个转入失败的处理接口


app.MapPost("/api/TransInError", (string branch_id, string gid, string op, TransRequest req) =>
    Console.WriteLine($"用户【req.UserId】转入【req.Amount】正向操作--失败,gid=gid, branch_id=branch_id, op=op");
    //return Results.BadRequest();
    return Results.Ok(TransResponse.BuildFailureResponse());
);

失败的返回有两种,一种是状态码大于 400,一种是状态码是 200 并且响应体包含 FAILURE,上面的例子是第二种


调整一下调用方,把转入正向操作替换成上面这个返回错误的接口。


var saga = new Saga(dtmClient, gid)
    .Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq)
    .Add(inApi + "/TransInError", inApi + "/TransInCompensate", userInReq);

运行结果如下:



在这个例子中,只考虑补偿/重试成功的情况下。


用户1 转出的 30 块钱最终是回到了他的帐号上,他没有出现损失。


用户2 就有点苦逼了,转入没有成功,返回了失败,还触发了转入的补偿机制,结果就是把用户2 还没进帐的 30 块钱给多扣了,这个就是上面的情况2,常见的空补偿问题。


这个时候就要在进行转入补偿的时候做一系列的判断,转入有没有成功,转出有没有失败等等,把业务变的十分复杂。


如果出现了上述的情况1,会发生什么呢?


用户2 第一次已经成功转入 30 块钱,返回的也是成功,但是网络出了点问题,导致 dtm 认为失败了,它就会进行重试,相当于用户2  还会收到第二个转入 30 块钱的请求!也就是说这次转帐,用户2 会进账 60 块钱,翻倍了,也就是说这个请求不是幂等。


同样的,要处理这个问题,在进行转入的正向操作中也要进行一系列的判断,同样会把复杂度上升一个级别。


前面有提到 dtm 提供了子事务屏障的功能,保证了幂等、空补偿等常见问题。



再来看看这个子事务屏障的功能有没有帮我们简化上面异常处理。


子事务屏障


子事务屏障,需要根据 trans_type,gid,branch_idop 四个内容进行创建。


这4个内容 dtm 在回调时会放在 querysting 上面。


客户端里面提供了 IBranchBarrierFactory 来供我们使用。


空补偿


针对上面的异常情况(用户2 凭空消失 30 块钱),对转入的补偿进行子事务屏障的改造。


app.MapPost("/api/BarrierTransInCompensate", async (string branch_id, string gid, string op, string trans_type, TransRequest req, IBranchBarrierFactory factory) =>
    var barrier = factory.CreateBranchBarrier(trans_type, gid, branch_id, op);
    using var db = Db.GeConn();
    await barrier.Call(db, async (tx) =>
    
        // 转入失败的情况下,不应该输出下面这个
        Console.WriteLine($"用户【req.UserId】转入【req.Amount】补偿操作,gid=gid, branch_id=branch_id, op=op");
        // tx 参数是事务,可和本地事务一起提交回滚
        await Task.CompletedTask;
    );
    Console.WriteLine($"子事务屏障-补偿操作,gid=gid, branch_id=branch_id, op=op");
    return Results.Ok(TransResponse.BuildSucceedResponse());
);

Call 方法就是关键所在了,需要传入一个 DbConnection 和真正的业务操作,这里的业务操作就是在控制台输出补偿操作的信息。


同样的,我们再调整一下调用方,把转入补偿操作替换成上面带子事务屏障的接口。


var saga = new Saga(dtmClient, gid)
    .Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq)
    .Add(inApi + "/TransInError", inApi + "/BarrierTransInCompensate", userInReq)
    ;

再来运行这个例子。



会发现转入的补偿操作并没执行,控制台没有输出补偿信息,而是输出了


Will not exec busiCall, isNullCompensation=True, isDuplicateOrPend=False

这个就表明了,这个请求是个空补偿,是不应该执行业务方法的,即空操作。


再来看一下,转入成功的,但是 dtm 收到了失败的信号,不断重试造成重复请求的情况。


幂等


针对用户2 转入两次 30 块钱的异常情况,对转入的正向操作进行子事务屏障的改造。


app.MapPost("/api/BarrierTransIn", async (string branch_id, string gid, string op, string trans_type, TransRequest req, IBranchBarrierFactory factory) =>
    Console.WriteLine($"用户【req.UserId】转入【req.Amount】请求来了!!!gid=gid, branch_id=branch_id, op=op");
    var barrier = factory.CreateBranchBarrier(trans_type, gid, branch_id, op);
    using var db = Db.GeConn();
    await barrier.Call(db, async (tx) =>
    
        var c = Interlocked.Increment(ref _errCount);
        // 模拟一个超时执行
        if (c > 0 && c < 2) await Task.Delay(10000);
        Console.WriteLine($"用户【req.UserId】转入【req.Amount】正向操作&#xff0c;gid&#61;gid, branch_id&#61;branch_id, op&#61;op");
        await Task.CompletedTask;
    );
    return Results.Ok(TransResponse.BuildSucceedResponse());
);

这里通过一个超时执行来让 dtm 进行转入正向操作的重试。


同样的&#xff0c;我们再调整一下调用方&#xff0c;把转入的正向操作也替换成上面带子事务屏障的接口。


var saga &#61; new Saga(dtmClient, gid)
    .Add(outApi &#43; "/TransOut", outApi &#43; "/TransOutCompensate", userOutReq)
    .Add(inApi &#43; "/BarrierTransIn", inApi &#43; "/BarrierTransInCompensate", userInReq)
    ;

再来运行这个例子。



可以看到转入的正向操作确实是触发了多次&#xff0c;第一次实际上是成功&#xff0c;只是响应比较慢&#xff0c;导致 dtm 认为是失败了&#xff0c;触发了第二次请求&#xff0c;但是第二次请求并没有执行业务操作&#xff0c;而是输出了


Will not exec busiCall, isNullCompensation&#61;False, isDuplicateOrPend&#61;True

这个就表明了&#xff0c;这个请求是个重复请求&#xff0c;是不应该执行业务方法的&#xff0c;保证了幂等。


到这里&#xff0c;可以看出&#xff0c;子事务屏障确实解决了幂等和空补偿的问题&#xff0c;大大降低了业务判断的复杂度和出错的可能性


写在最后


在这篇文章里&#xff0c;也通过几个例子&#xff0c;完整给出了编写一个 SAGA 事务的过程&#xff0c;涵盖了正常成功完成&#xff0c;异常情况&#xff0c;以及成功回滚的情况。希望对研究分布式事务的您有所帮助。


本文示例代码&#xff1a;https://github.com/catcherwong-archive/2022/tree/main/DtmSagaSample


参考资料


  • https://segmentfault.com/a/1190000040294095

  • https://segmentfault.com/a/1190000040515787

  • https://github.com/dtm-labs/dtmcli-csharp


推荐阅读
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 导出功能protectedvoidbtnExport(objectsender,EventArgse){用来打开下载窗口stringfileName中 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
author-avatar
双眼皮的猪猪2012
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有